package com.such.kit.web.jwt.filter;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.lang.JoseException;
import org.springframework.web.context.WebApplicationContext;
import com.alibaba.fastjson.JSON;
import com.such.kit.Logger;
import com.such.kit.datacast.CastCommons;
import com.such.kit.string.StringCommons;
import com.such.kit.validate.ValidateCommons;
import com.such.kit.web.bean.WebResponse;
import com.such.kit.web.jwt.JWTCommons;
import com.such.kit.web.jwt.bean.JWTConfig;
import com.such.kit.web.jwt.bean.PayloadStorer;
import com.such.kit.web.jwt.bean.ValidateResult;

/**
 * @author SUCH <pre>
 * token 拦截器
 * 
 * init 参数列表：
 * configBeanId JWTConfig spring beanId [必填]
 * realtimeToken 实时更新令牌，每次通过 AuthorizedFilter 都会生成新的令牌 [可空] [true | false] [默认值为 false]
 * payloadKeys 指定 payload 中添加的自定义属性，以便生成新 token 时自动获取、填充，多个 key 以 , 分隔 [可空]
 * excludeMapping 排除路径，多个路径以换行分隔 [可空]
 * 
 * 内置的错误码：
 * 10001 令牌非法（无法解析）
 * 10002 令牌过期（由过期导致的验证失败）
 * 10003 令牌失效（其它原因导致的验证失败）
 * </pre>
 */
public class AuthorizedFilter implements Filter {

	private WebApplicationContext springContext;
	private JWTConfig jwtConfig;
	private boolean realtimeToken;
	private Set<String> payloadKeys = new HashSet<String>();
	private Set<String> excludeMapping = new HashSet<String>();

	@Override
	public void init(FilterConfig config) throws ServletException {
		ServletContext servletContext = config.getServletContext();
		this.springContext = (WebApplicationContext) servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
		String jwtConfigBeanId = config.getInitParameter("jwtConfigBeanId");
		String realtimeToken = config.getInitParameter("realtimeToken");
		String payloadKeys = config.getInitParameter("payloadKeys");
		String excludeMapping = config.getInitParameter("excludeMapping");
		if (ValidateCommons.isNotEmpty(jwtConfigBeanId)) {
			this.jwtConfig = this.springContext.getBean(jwtConfigBeanId, JWTConfig.class);
		} else {
			throw new ServletException("请指定 jwtConfigBeanId");
		}
		if (ValidateCommons.isNotEmpty(realtimeToken)) {
			this.realtimeToken = CastCommons.stringToBoolean(realtimeToken);
		}
		if (ValidateCommons.isNotEmpty(payloadKeys)) {
			String[] keys = payloadKeys.split(",");
			if (ValidateCommons.isNotEmpty(keys)) {
				for (String key : keys) {
					key = key.trim();
					if (ValidateCommons.isNotEmpty(key)) {
						this.payloadKeys.add(key);
					}
				}
			}
		}
		if (ValidateCommons.isNotEmpty(excludeMapping)) {
			String[] excludes = excludeMapping.split(StringCommons.NEW_LINE);
			if (ValidateCommons.isNotEmpty(excludes)) {
				String contextPath = config.getServletContext().getContextPath();
				for (String exclude : excludes) {
					exclude = exclude.trim();
					if (ValidateCommons.isNotEmpty(exclude)) {
						this.excludeMapping.add(StringCommons.merger(contextPath, exclude));
					}
				}
			}
		}
	}

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		String uri = request.getRequestURI();
		HttpServletResponse response = (HttpServletResponse) res;
		ServletOutputStream out = response.getOutputStream();

		// 处理排除 uri
		if (this.excludeMapping.contains(uri)) {
			chain.doFilter(request, response);
			return;
		}

	    // doFilter 之前处理请求
		// 由于不同的浏览器、中间件会对头不 key 做不同的命名处理，tokenKey 统一转为小写，前台获取时也需要进行相应处理
		String tokenKey = StringCommons.toLowerCase(this.jwtConfig.getKey());
		String token = request.getHeader(tokenKey);
		if (ValidateCommons.isEmpty(token)) {
			out.print(JSON.toJSONString(new WebResponse<Boolean>(10001, "令牌非法")));
			return;
		}
		JWTCommons jwt = JWTCommons.getInstance(this.jwtConfig);
		ValidateResult result = jwt.validate(token);
		if (!result.authenticated()) {
			Logger.warn(getClass(), result.getMessage());
			if (result.expired()) {
				out.print(JSON.toJSONString(new WebResponse<Boolean>(10002, "令牌过期")));
			} else {
				out.print(JSON.toJSONString(new WebResponse<Boolean>(10003, "令牌失效")));
			}
			return;
		}
		JwtClaims claims = result.getJwtClaims();
		PayloadStorer.setPayload(claims);
		if (this.realtimeToken) {
			try {
				Map<String, Object> payload = new HashMap<String, Object>();
				if (ValidateCommons.isNotEmpty(this.payloadKeys)) {
					Map<String, Object> claimsMap = claims.getClaimsMap();
					for (String payloadKey : this.payloadKeys) {
						payload.put(payloadKey, claimsMap.get(payloadKey));
					}
				}
				response.setHeader(tokenKey, jwt.generate(payload));
			} catch (JoseException e) {
				Logger.error(getClass(), "更新令牌异常", e);
			}
		}
		chain.doFilter(request, response);
	    // doFilter 之后处理响应
	}

	@Override
	public void destroy() {
	}

}
